Street Coder
Authors: Sedat Kapanoğlu, Sedat Kapanoğlu
Overview
Street Coder isn’t your typical software development book full of best practices. It’s about the unvarnished realities of professional coding – what I call ‘the streets’. This book is for self-taught programmers and recent graduates who’ve learned the rules but lack the practical wisdom of when to break them.
This book isn’t about regurgitating textbook knowledge. It’s a collection of insights I’ve gained over 25 years, from my early days as a self-taught coder in Turkey, through my time at Microsoft, to running my own popular social platform. I’m sharing the hard lessons and ‘street smarts’ that formal education often misses, like when it’s okay to write ‘bad’ code, how to make friends with anti-patterns, and why ‘premature optimization’ can actually be a valuable learning tool.
Throughput is king in the real world. I emphasize prioritizing results, embracing complexity, and designing with a constant eye towards performance and scalability. I delve into essential theory – like understanding data structures, algorithms, and Big-O notation – not as abstract concepts but as practical tools for making smarter coding decisions. I explore security from a reliability perspective, teaching you to build defenses against common vulnerabilities like SQL injection and cross-site scripting. I encourage questioning everything – from best practices to the sacred SOLID principles – and finding what actually works for you.
I even dive into the nitty-gritty of C# and .NET, showing you hidden gems and efficient techniques that can boost your coding speed and effectiveness, like how to leverage value types, avoid unnecessary lock statements, and exploit the power of asynchronous I/O. I also equip you with soft skills, like how to navigate code reviews, manage a demanding boss, and even leverage rubber duck debugging.
This book isn’t for seasoned experts. It’s for those just starting their journey, ready to get their hands dirty and learn the unwritten rules of surviving – and thriving – in the streets of professional software development.
Book Outline
1. To the streets
What truly matters in professional software development is your throughput—how much you can deliver in a given time. While elegant design, algorithms, and high-quality code are important, they are means to an end, not the end itself. Your colleagues care about maintainable and easily understandable code. Prioritizing these qualities can boost the team’s throughput, which directly impacts product success and career progression.
Key concept: Rubber duck debugging is a method for finding solutions to programming problems by talking to a yellow plastic bird.
2. Practical theory
While libraries and frameworks typically handle optimization, understanding fundamental computer science theory like algorithms and data structures helps you make better decisions about which ones to use and when to implement one from scratch. Understanding Big-O notation, types, and how data structures work internally are key for a developer’s efficiency.
Key concept: Big-O notation is a way to describe how the resources used by an algorithm—both time and memory—scale with increasing input size. It’s about growth trends, not absolute values.
3. Useful anti-patterns
Sometimes, ‘bad’ practices or anti-patterns can be useful. For example, breaking working code intentionally to identify hidden problems with dependencies, rewriting code from scratch when stuck, fixing code even if it isn’t broken to prevent future issues, and repeating code instead of over-reusing it to avoid tightly coupled components. Question the utility of SOLID principles.
Key concept: Dependencies are a two-clause contract: while component B provides services to component A, component A must also undergo maintenance whenever B introduces a breaking change. Over-reliance on code reuse creates rigidity and resistance to change.
4. Tasty testing
Testing should improve your life, not make it miserable. Avoid dogmatic testing methodologies like TDD. Write tests for yourself to gain confidence in your code and to enable future changes, focusing on boundary conditions and using types to reduce the need for some tests. Manual testing, especially of core user flows, and some testing in production, in specific situations, can also be beneficial.
Key concept: Tests are like preflight checks or ‘system nominal’ status reports that provide assurance and allow you to iterate more confidently.
5. Rewarding refactoring
Refactoring is about changing the structure of code to make it more adaptable and maintainable. For architectural changes, an incremental approach is best, starting by identifying common components that can be extracted to minimize disruptions. Use tests to ensure reliability throughout the refactoring process. Dependency injection is a valuable tool for decoupling code and enabling flexibility.
Key concept: Refactoring without disrupting colleagues is like changing a tire while driving: make the old disappear and replace it with the new, seamlessly.
6. Security by scrutiny
Security is not just about hackers and vulnerabilities. It’s also about how your data can be misused or how seemingly unrelated factors can cause harm. Threat modeling, even in a simplified form, helps prioritize security measures and assess risks. Write secure code by focusing on minimizing attack vectors, leveraging existing security solutions, and avoiding premature optimization of security-related code.
Key concept: Security is a race against time; its goal is to make an attacker’s job harder.
7. Opinionated optimization
Premature optimization can be detrimental, but it can also be a learning experience. Focus on solving the right performance problems, not hypothetical ones. Start with high-level optimization like algorithm selection before diving into low-level code tweaks. Use benchmarking to measure the impact of your optimizations. Optimize for responsiveness and user experience alongside raw speed.
Key concept: Premature optimization is the root of all learning, or rather, “We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.”
8. Palatable scalability
Scalability is about maintaining responsiveness under increasing demand. Avoid locks whenever possible, or use lock-free data structures or other techniques like double-checked locking to minimize their impact. Embrace eventual consistency for better scalability and performance. Don’t over-use threads and leverage asynchronous I/O for non-blocking operations whenever suitable. Respect the monolith and avoid the complexity of microservices prematurely.
Key concept: Scalability is best addressed progressively in tangible, smaller steps toward a bigger goal.
9. Living with bugs
Bugs are inevitable. Triage bugs by impact and severity to focus on what truly matters. Don’t treat all errors as exceptions. Use exceptions for truly exceptional scenarios, and consider using enums for expected error cases. Use printf debugging, tracing, or crash dump analysis for more effective bug identification, and leverage rubber-duck debugging as a first step.
Key concept: Exceptions are for the exceptional, not for regular flow control.
Essential Questions
1. What truly matters in the “streets” of professional software development?
The reality of “the streets,” or the professional software world, often contrasts sharply with academic learning and idealistic best practices. In the streets, throughput – getting things done – is paramount. While good design and code quality are crucial, they serve the ultimate goal of shipping products. Street coders, therefore, are pragmatic, results-oriented, and capable of balancing idealism with the need to deliver. They question established norms, adapt to shifting requirements, and aren’t afraid to take calculated risks, like intentionally breaking code or testing in production when appropriate, or using goto, to learn and optimize.
2. Why is understanding computer science theory important, even when using frameworks and libraries?
While frameworks and libraries provide abstractions that simplify development, understanding the underlying theory is crucial for making informed decisions and avoiding costly pitfalls. Knowing how data structures are laid out, the characteristics of algorithms, the implications of Big-O notation, and the nuances of types can significantly impact performance, security, and maintainability. This foundational knowledge empowers developers to choose the right tool for the job, optimize code effectively, and avoid reinventing the wheel inefficiently.
3. When can “bad” practices actually be useful in software development?
Rigidity in code, often caused by over-reliance on code reuse and tightly coupled components, can hinder adaptability and make even small changes difficult. Embracing “bad” practices like intentionally breaking working code to expose dependencies or rewriting code from scratch when a design becomes convoluted can, counterintuitively, lead to more robust and maintainable solutions in the long run. These practices, when applied judiciously, enable quicker iterations, clearer understanding of the codebase, and reduced technical debt. They promote continuous learning by exposing developers to the consequences of their design decisions.
4. How can testing be made more enjoyable and beneficial for the developer?
Testing should not be a tedious chore but a valuable tool for improving both the software and the developer. Instead of dogmatic approaches like TDD, focus on writing tests that increase your confidence in your code and make it easier to make future changes. This means prioritizing tests for your benefit, not for the sake of ticking a box. Concentrate on boundary conditions, leverage types to reduce the need for certain tests, and even consider some manual or production testing in specific scenarios.
5. What is the best way to approach refactoring, especially for large architectural changes?
Refactoring is not merely a cleanup activity but a powerful tool for improving code structure, reducing technical debt, and making large architectural changes feasible. The key is to approach it strategically, planning for incremental changes and isolating common components to minimize disruptions. Use tests to ensure reliability, and leverage dependency injection to decouple components and enhance flexibility. Remember that the goal is to make future changes easier and more efficient, so avoid premature refactoring.
1. What truly matters in the “streets” of professional software development?
The reality of “the streets,” or the professional software world, often contrasts sharply with academic learning and idealistic best practices. In the streets, throughput – getting things done – is paramount. While good design and code quality are crucial, they serve the ultimate goal of shipping products. Street coders, therefore, are pragmatic, results-oriented, and capable of balancing idealism with the need to deliver. They question established norms, adapt to shifting requirements, and aren’t afraid to take calculated risks, like intentionally breaking code or testing in production when appropriate, or using goto, to learn and optimize.
2. Why is understanding computer science theory important, even when using frameworks and libraries?
While frameworks and libraries provide abstractions that simplify development, understanding the underlying theory is crucial for making informed decisions and avoiding costly pitfalls. Knowing how data structures are laid out, the characteristics of algorithms, the implications of Big-O notation, and the nuances of types can significantly impact performance, security, and maintainability. This foundational knowledge empowers developers to choose the right tool for the job, optimize code effectively, and avoid reinventing the wheel inefficiently.
3. When can “bad” practices actually be useful in software development?
Rigidity in code, often caused by over-reliance on code reuse and tightly coupled components, can hinder adaptability and make even small changes difficult. Embracing “bad” practices like intentionally breaking working code to expose dependencies or rewriting code from scratch when a design becomes convoluted can, counterintuitively, lead to more robust and maintainable solutions in the long run. These practices, when applied judiciously, enable quicker iterations, clearer understanding of the codebase, and reduced technical debt. They promote continuous learning by exposing developers to the consequences of their design decisions.
4. How can testing be made more enjoyable and beneficial for the developer?
Testing should not be a tedious chore but a valuable tool for improving both the software and the developer. Instead of dogmatic approaches like TDD, focus on writing tests that increase your confidence in your code and make it easier to make future changes. This means prioritizing tests for your benefit, not for the sake of ticking a box. Concentrate on boundary conditions, leverage types to reduce the need for certain tests, and even consider some manual or production testing in specific scenarios.
5. What is the best way to approach refactoring, especially for large architectural changes?
Refactoring is not merely a cleanup activity but a powerful tool for improving code structure, reducing technical debt, and making large architectural changes feasible. The key is to approach it strategically, planning for incremental changes and isolating common components to minimize disruptions. Use tests to ensure reliability, and leverage dependency injection to decouple components and enhance flexibility. Remember that the goal is to make future changes easier and more efficient, so avoid premature refactoring.
Key Takeaways
1. Prioritize Throughput
Focus on results, not just elegant solutions. Use the right tool for the job, not the shiniest. Start simple, measure, and then optimize where necessary, based on data not hunches. This pragmatic approach maximizes throughput and minimizes wasted effort.
Practical Application:
When designing an AI model, instead of immediately reaching for the most complex architecture, start with a simpler model and benchmark its performance. If it meets the requirements, you’ve saved time and effort. If not, profile the model to identify bottlenecks and then iteratively optimize or refactor specific parts based on data rather than assumptions. This can involve using more efficient data structures, tweaking algorithms, or employing techniques like SIMD to parallelize computations.
2. Embrace Abstraction
Avoid tight coupling to external libraries or frameworks. Create abstractions to interact with them, decoupling your code and providing flexibility to swap implementations without major rewrites.
Practical Application:
When integrating a new machine learning library into your project, avoid directly tying your code to its API. Instead, create an abstraction layer (an interface or adapter) that defines the functionality you need. This way, if you need to switch libraries or change its underlying implementation, you only need to modify the adapter, not your entire codebase. This decouples your code and makes it more flexible and maintainable.
3. Design with Security in Mind
Don’t just focus on functionality; consider security from the start. Understand how seemingly harmless code can lead to vulnerabilities and implement countermeasures. Don’t rely solely on security by obscurity. Prioritize security based on potential threats. Design with a threat model in mind.
Practical Application:
When designing an AI application, build security in from the beginning by considering the potential ways your models or data could be misused or attacked. Implement parameterized queries to avoid SQL injections when interacting with databases, sanitize and encode user inputs to avoid XSS vulnerabilities in web interfaces, and store sensitive API keys securely outside the codebase. Use proven security libraries.
4. Optimize for Performance
Understand how hardware limitations like memory access, I/O bottlenecks, and CPU pipeline stalls affect performance. Optimize data structures, algorithms, and code to minimize these limitations. Use tools like profilers and benchmarks to measure and identify specific bottlenecks. Use CPU features like SIMD to parallelize tasks.
Practical Application:
When dealing with massive datasets for training AI models, optimize data storage, access patterns, and processing to minimize delays. Consider using data structures like dictionaries (hash tables) for fast lookups, optimize I/O by buffering large reads and writes, and use techniques like SIMD or asynchronous I/O to parallelize tasks and maximize throughput. This can involve experimenting with different buffer sizes to maximize efficiency. Ensure consistent hash values to avoid dictionary performance degradation.
5. Respect the Monolith
Start with a monolith, not microservices. Focus on building a functional system first and only break it into smaller parts as the need for independent scaling or isolation arises. Microservices introduce complexity that should be avoided unless absolutely necessary.
Practical Application:
When developing a complex AI system, resist the urge to immediately split it into numerous microservices. Start with a monolithic architecture, focusing on building a functional system first. As the system grows and the need for scalability arises, identify specific parts that would benefit from separation and gradually refactor them into independent services. This minimizes initial complexity and allows you to focus on delivering value early on.
1. Prioritize Throughput
Focus on results, not just elegant solutions. Use the right tool for the job, not the shiniest. Start simple, measure, and then optimize where necessary, based on data not hunches. This pragmatic approach maximizes throughput and minimizes wasted effort.
Practical Application:
When designing an AI model, instead of immediately reaching for the most complex architecture, start with a simpler model and benchmark its performance. If it meets the requirements, you’ve saved time and effort. If not, profile the model to identify bottlenecks and then iteratively optimize or refactor specific parts based on data rather than assumptions. This can involve using more efficient data structures, tweaking algorithms, or employing techniques like SIMD to parallelize computations.
2. Embrace Abstraction
Avoid tight coupling to external libraries or frameworks. Create abstractions to interact with them, decoupling your code and providing flexibility to swap implementations without major rewrites.
Practical Application:
When integrating a new machine learning library into your project, avoid directly tying your code to its API. Instead, create an abstraction layer (an interface or adapter) that defines the functionality you need. This way, if you need to switch libraries or change its underlying implementation, you only need to modify the adapter, not your entire codebase. This decouples your code and makes it more flexible and maintainable.
3. Design with Security in Mind
Don’t just focus on functionality; consider security from the start. Understand how seemingly harmless code can lead to vulnerabilities and implement countermeasures. Don’t rely solely on security by obscurity. Prioritize security based on potential threats. Design with a threat model in mind.
Practical Application:
When designing an AI application, build security in from the beginning by considering the potential ways your models or data could be misused or attacked. Implement parameterized queries to avoid SQL injections when interacting with databases, sanitize and encode user inputs to avoid XSS vulnerabilities in web interfaces, and store sensitive API keys securely outside the codebase. Use proven security libraries.
4. Optimize for Performance
Understand how hardware limitations like memory access, I/O bottlenecks, and CPU pipeline stalls affect performance. Optimize data structures, algorithms, and code to minimize these limitations. Use tools like profilers and benchmarks to measure and identify specific bottlenecks. Use CPU features like SIMD to parallelize tasks.
Practical Application:
When dealing with massive datasets for training AI models, optimize data storage, access patterns, and processing to minimize delays. Consider using data structures like dictionaries (hash tables) for fast lookups, optimize I/O by buffering large reads and writes, and use techniques like SIMD or asynchronous I/O to parallelize tasks and maximize throughput. This can involve experimenting with different buffer sizes to maximize efficiency. Ensure consistent hash values to avoid dictionary performance degradation.
5. Respect the Monolith
Start with a monolith, not microservices. Focus on building a functional system first and only break it into smaller parts as the need for independent scaling or isolation arises. Microservices introduce complexity that should be avoided unless absolutely necessary.
Practical Application:
When developing a complex AI system, resist the urge to immediately split it into numerous microservices. Start with a monolithic architecture, focusing on building a functional system first. As the system grows and the need for scalability arises, identify specific parts that would benefit from separation and gradually refactor them into independent services. This minimizes initial complexity and allows you to focus on delivering value early on.
Memorable Quotes
1.2 Who’s a street coder?. 3
Be it a self-taught programmer or someone who studied computer science, they are missing a common piece at the beginning of their career: street lore, which is the expertise to know what matters most.
1.3.1 Questioning. 5
Many books […] emphasize the importance of being critical and inquisitive, but few of them give you something to work with.
1.3.2 Results-driven. 29
You can be the best programmer in the world […], but those will mean nothing if you are not shipping, if you are not getting the product out.
3.2.1 Erase and rewrite. 61
I say, start from scratch: rewrite it. Toss away everything you already did and write every bit from scratch. You can’t imagine how refreshing and fast that will be.
7. Solve the right problem. 171
I claim that premature optimization is the root of all learning.
1.2 Who’s a street coder?. 3
Be it a self-taught programmer or someone who studied computer science, they are missing a common piece at the beginning of their career: street lore, which is the expertise to know what matters most.
1.3.1 Questioning. 5
Many books […] emphasize the importance of being critical and inquisitive, but few of them give you something to work with.
1.3.2 Results-driven. 29
You can be the best programmer in the world […], but those will mean nothing if you are not shipping, if you are not getting the product out.
3.2.1 Erase and rewrite. 61
I say, start from scratch: rewrite it. Toss away everything you already did and write every bit from scratch. You can’t imagine how refreshing and fast that will be.
7. Solve the right problem. 171
I claim that premature optimization is the root of all learning.
Comparative Analysis
Street Coder distinguishes itself from traditional software development books like “Clean Code” by Robert Martin or “Design Patterns” by the Gang of Four. While those books focus on establishing and adhering to best practices and design patterns, Street Coder acknowledges their value but emphasizes the importance of knowing when to deviate from them in the interest of pragmatism and throughput. It complements these classic texts by providing a perspective on how to balance theoretical ideals with the realities of deadlines, changing requirements, and the constant pressure to deliver results. Unlike books solely focusing on specific languages or frameworks, Street Coder takes a more language-agnostic approach, though it uses C# and .NET for examples. This makes the book’s core message applicable to a wider audience of developers. It diverges from academic texts that prioritize theoretical rigor by emphasizing practical application and experiential learning, acknowledging the value of ‘street smarts’ gained through trial and error. While many books promote DRY (Don’t Repeat Yourself), Street Coder encourages repeating code to avoid tightly coupled components which complements those texts.
Reflection
Street Coder provides a valuable counterpoint to the often-idealized view of software development presented in many books and courses. Its focus on pragmatism, throughput, and the acceptance of ‘good enough’ can be especially beneficial for AI product engineers working in fast-paced environments. The book’s insights on code optimization, security, and scalability are directly applicable to the challenges of building and deploying AI applications, especially concerning performance and data management. However, its dismissive attitude towards certain best practices and formal processes should be taken with a grain of salt. While prioritizing throughput is essential, blindly applying “bad” practices without careful consideration can lead to long-term technical debt and security risks. The book’s strength lies in its encouragement to question assumptions and find what truly works, but this should be balanced with a respect for proven methodologies and established standards. The book’s anecdotal style, while entertaining, sometimes lacks the depth and rigor required for a complete understanding of complex topics. Its focus on C# and .NET may also limit its relevance to AI engineers using other technology stacks. Despite these limitations, Street Coder offers a fresh and pragmatic perspective that can help AI engineers become more efficient, resilient, and ultimately, more successful in their careers.
Flashcards
What is the time complexity of looking up an item in a hash table (dictionary) in the average case?
O(1)
What is the time complexity of searching for an item in a linked list?
O(N)
What is the time complexity of a binary search in a sorted array?
O(log N)
What matters most in professional software development, according to the author?
Throughput
What is a value type (in the context of this book)?
A class that validates its input upon construction and prevents invalid states.
What is Big-O notation?
A way to describe the growth trend of an algorithm’s resource usage with increasing input size.
Name three common web application security vulnerabilities.
SQL injection, Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF)
What analogy is used to describe the ideal refactoring process?
Changing a tire while driving on a highway.
What is pipelining (in the context of CPU optimization)?
A technique to separate computationally independent parts of your code to improve concurrency and allow the CPU to better utilize its pipeline.
What does LIFO stand for, and what does it mean in relation to stacks?
Last In, First Out. It means when removing an item from this data structure, it’ll always return the latest added one.
What is the time complexity of looking up an item in a hash table (dictionary) in the average case?
O(1)
What is the time complexity of searching for an item in a linked list?
O(N)
What is the time complexity of a binary search in a sorted array?
O(log N)
What matters most in professional software development, according to the author?
Throughput
What is a value type (in the context of this book)?
A class that validates its input upon construction and prevents invalid states.
What is Big-O notation?
A way to describe the growth trend of an algorithm’s resource usage with increasing input size.
Name three common web application security vulnerabilities.
SQL injection, Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF)
What analogy is used to describe the ideal refactoring process?
Changing a tire while driving on a highway.
What is pipelining (in the context of CPU optimization)?
A technique to separate computationally independent parts of your code to improve concurrency and allow the CPU to better utilize its pipeline.
What does LIFO stand for, and what does it mean in relation to stacks?
Last In, First Out. It means when removing an item from this data structure, it’ll always return the latest added one.